package org.hackystat.sdsa.service.resource.devstream;

import static org.hackystat.sdsa.service.server.Authenticator.AUTHENTICATOR_SENSORBASE_CLIENTS_KEY;
import static org.hackystat.sdsa.service.server.ServerProperties.SENSORBASE_FULLHOST_KEY;

import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;
import java.util.logging.Logger;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.hackystat.sdsa.analyzer.SdsaAnalyzerException;
import org.hackystat.sdsa.analyzer.control.DevStreamBuilder;
import org.hackystat.sdsa.service.resource.devstream.jaxb.DevStream;
import org.hackystat.sdsa.service.server.Server;
import org.hackystat.sdsa.service.server.ServerProperties;
import org.hackystat.sensorbase.client.SensorBaseClient;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorData;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataIndex;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataRef;
import org.hackystat.utilities.stacktrace.StackTrace;
import org.hackystat.utilities.tstamp.Tstamp;
import org.hackystat.utilities.uricache.UriCacheException;
import org.restlet.Context;
import org.restlet.data.CharacterSet;
import org.restlet.data.Language;
import org.restlet.data.MediaType;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.data.Status;
import org.restlet.resource.Representation;
import org.restlet.resource.Resource;
import org.restlet.resource.StringRepresentation;
import org.restlet.resource.Variant;
import org.w3c.dom.Document;

/**
 * Processes GET {host}/sdsa/devstream/{tokenizer}/{ruleset}/{email}/{project}/{start}/{end} 
 * requests. Requires the authenticated user to be {email} or else the Admin user for the 
 * sensorbase connected to this service. 
 * 
 * @author Hongbing Kou
 */
public class DevStreamResource extends Resource {
  /** To be retrieved from the URL as the 'email' template parameter, or null. */
  protected String uriUser = null; 

  /** To be retrieved from the URL as the 'project' template parameter, or null. */
  protected String project = null; 

  /** To be retrieved from the URL as the 'start' template parameter, or null. */
  protected String start = null; 

  /** To be retrieved from the URL as the 'end' template parameter, or null. */
  protected String end = null; 

  /** To be retrieved from the URL as the 'tokenizer' template parameter, or null. */
  protected String tokenizer = null; 

  /** To be retrieve from the URL as the 'ruleset' template parameter, or null. */
  protected String ruleset = null;
  

  /** The authenticated user, retrieved from the ChallengeResponse, or null */
  protected String authUser = null;
  
  /** This server (sdsa). */
  protected Server sdsaServer;
  
  /** The sensorbase host (for authentication). */
  protected String sensorBaseHost;
  
  /** The standard error message returned from invalid authentication. */
  protected String badAuth = "User is not admin and authenticated user does not match URI user";

  /**
   * The standard constructor.
   * @param context The context.
   * @param request The request object.
   * @param response The response object.
   */
  public DevStreamResource(Context context, Request request, Response response) {
    super(context, request, response);

    if (request.getChallengeResponse() != null) {
      this.authUser = request.getChallengeResponse().getIdentifier();
    }
    this.sdsaServer = (Server)getContext().getAttributes().get("SdsaServer");
    Logger logger = this.sdsaServer.getLogger(); 
    logger.info("\n*********************************************************");
    logger.info("SDSA: Accepted a new request for DevStream");

    ServerProperties properties = this.sdsaServer.getServerProperties();
    this.sensorBaseHost = properties.get(SENSORBASE_FULLHOST_KEY);
    this.uriUser = (String) request.getAttributes().get("email");
    this.project = (String) request.getAttributes().get("project");
    this.start = (String) request.getAttributes().get("start");
    this.end = (String) request.getAttributes().get("end");

    this.tokenizer = (String) request.getAttributes().get("tokenizer");
    this.ruleset = (String) request.getAttributes().get("ruleset");

    logger.info("\tTokenizer = " + this.tokenizer);
    logger.info("\truleset    = " + this.ruleset);
    logger.info("\tUser      = " + this.uriUser);
    logger.info("\tProject   = " + this.project);
    logger.info("\tStart     = " + this.start);
    logger.info("\tEnd       = " + this.end);

    getVariants().clear(); // copied from BookmarksResource.java, not sure why needed.
    getVariants().add(new Variant(MediaType.TEXT_XML));
 }
  
  /**
   * Returns a SensorBaseClient instance associated with the User in this request. 
   * @return The SensorBaseClient instance. 
   */
  @SuppressWarnings("unchecked")
  public SensorBaseClient getSensorBaseClient() {
    Map<String, SensorBaseClient> userClientMap = 
      (Map<String, SensorBaseClient>)this.sdsaServer.getContext()
      .getAttributes().get(AUTHENTICATOR_SENSORBASE_CLIENTS_KEY);
    return userClientMap.get(this.authUser);
  }

  
  /**
   * Returns an TelemetryChart instance representing the trend information associated with the 
   * Project data, or null if not authorized. 
   * @param variant The representational variant requested.
   * @return The representation. 
   */
  @Override
  /*public Representation getRepresentation(Variant variant) {
    if (variant.getMediaType().equals(MediaType.TEXT_XML)) {
      try {
        // [1] perform authorization.
        // [2] get the SDSA parameters from the call.
        // [3] invoke the SDSA's development stream builder to get a complete DevStream.
        DevStream devStream = makeDevStream();
        // [4] package the resulting data up in XML and return it.
        return this.getStringRepresentation(devStream);
       }
      catch (Exception e) {
        this.sdsaServer.getLogger().warning("Error in chart: " + StackTrace.toString(e));
        return null;
      }
    }
    return null;
  }*/

  /**
   * Returns an DevStream instance representing the trend information associated with the 
   * Project data, or null if not authorized. 
   * @param variant The representational variant requested.
   * @return The representation. 
   */
  public Representation getRepresentation(Variant variant) {
    if (variant.getMediaType().equals(MediaType.TEXT_XML)) {
        // [1] get the SensorBaseClient for the user making this request.
        SensorBaseClient client = this.getSensorBaseClient();
        // Enables caching so that we don't repetitively phone sensorbase home for same data entry. 
        try {
          client.enableCaching();
        }
        catch (UriCacheException e) {
          String message = "Can not start the URI Cache: " + StackTrace.toString(e);
          getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST, message);
          this.sdsaServer.getLogger().warning(message);
          return null;
        }
        catch (IOException e) {
          String message = "IO problem in starting the URI cache: " + StackTrace.toString(e);
          getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST, message);
          this.sdsaServer.getLogger().warning(message);
          return null;
        }

        XMLGregorianCalendar startTime;
        try {
          startTime = Tstamp.makeTimestamp(this.start);
        }
        catch (Exception e) {
          String message = "The start time '" + this.start + "' is not valid: " + 
                           StackTrace.toString(e);
          getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST, message);
          this.sdsaServer.getLogger().warning(message);
          return null;
        }
        
        XMLGregorianCalendar endTime;
        try {
          endTime = Tstamp.makeTimestamp(this.end);
        }
        catch (Exception e) {
          String message = "The end time '" + this.end + "' is not valid: " 
                          + StackTrace.toString(e);
          getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST, message);
          this.sdsaServer.getLogger().warning(message);
          return null;
        }
        
        DevStreamBuilder devStreamBuilder;
        try {
          
          this.sdsaServer.getLogger().info("Instantiating DevStreamBuilder now.");
          devStreamBuilder = new DevStreamBuilder(this.tokenizer, this.ruleset,
              this.sdsaServer.getLogger()); 
        }
        catch (Exception e) {
          String message = "Problem in creating development stream: " + StackTrace.toString(e);
          getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST, message);
          this.sdsaServer.getLogger().warning(message);
          return null;
        }
        
        // [2] get a SensorDataIndex of all sensor data for this Project on the requested day.
        try {
          this.sdsaServer.getLogger().info("SDSA:  Processing DevEvents.");
          SensorDataIndex index = client.getProjectSensorData(authUser, project, startTime, 
              endTime);
          this.sdsaServer.getLogger().info("SDSA: Number of SensorDataRef = " + 
              index.getSensorDataRef().size());
  
          // [3] look through this index for DevEvent sensor data, and put them into 
          // an ActivityBuilder. 
          for (SensorDataRef ref : index.getSensorDataRef()) {
            if (ref.getSensorDataType().equals("DevEvent")) {
              // If it's a devEvent, process it to infer development activities.
              SensorData devEvent = client.getSensorData(ref);
              devStreamBuilder.addDevEvent(devEvent);
            }
          }
        }
        catch (SdsaAnalyzerException e) {
          String message = "Can not process one sensordata: " + StackTrace.toString(e);
          getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST, message);
          this.sdsaServer.getLogger().warning(message);
          return null;
        }
        catch (Exception e) {
          String message = "Error while fetching data from sensorbase: " + StackTrace.toString(e);
          getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST, message);
          this.sdsaServer.getLogger().warning(message);
          return null;
        }

        this.sdsaServer.getLogger().info("SDSA:  Building DevStream now.");
        // [4] Tokenizes the development activities
        DevStream devStream;
        try {
          devStream = devStreamBuilder.build();
        }
        catch (SdsaAnalyzerException e) {
          String message = "Could construct the DevStream from sensor data: " 
                           + StackTrace.toString(e);
          getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST, message);
          this.sdsaServer.getLogger().warning(message);
          return null;
        }
        
        // [5] Recognizes the micro and macro development behaviors.
        
        // [6] create and return the DevStream
        this.sdsaServer.getLogger().info("SDSA:  Wrapping the DevStream in XML representation.");
        String xmlData;
        try {
          xmlData = makeXmlData(devStream);
        }
        catch (Exception e) {
          String message = "DevStream business object is ready but could not " + 
          " transfer it to XML data : " + StackTrace.toString(e);
          getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST, message);
          this.sdsaServer.getLogger().warning(message);
          return null;
        }
        
        return getStringRepresentation(xmlData);
    }
    else {
      String msg = "Can not process media type : " + variant.getMediaType();
      getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST, msg);
      this.sdsaServer.getLogger().warning(msg);
    }
    
    return null;
  }
  
  /**
   * Creates and returns a new Restlet StringRepresentation built from xmlData.
   * The xmlData will be prefixed with a processing instruction indicating UTF-8 and version 1.0.
   * @param xmlData The xml data as a string. 
   * @return A StringRepresentation of that xmldata. 
   */
  public StringRepresentation getStringRepresentation(String xmlData) {
    return new StringRepresentation(xmlData, MediaType.TEXT_XML, Language.ALL, CharacterSet.UTF_8);
  }
  
  /**
   * Returns the passed DevStream instance as a String encoding of its XML representation.
   * @param data The SensorData instance. 
   * @return The XML String representation.
   * @throws Exception If problems occur during translation. 
   */
  private String makeXmlData (DevStream data) throws Exception {
    JAXBContext devTimeJAXB = 
      (JAXBContext)this.sdsaServer.getContext().getAttributes().get("DevStreamJAXB");
    Marshaller marshaller = devTimeJAXB.createMarshaller(); 
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    dbf.setNamespaceAware(true);
    DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
    Document doc = documentBuilder.newDocument();
    marshaller.marshal(data, doc);
    DOMSource domSource = new DOMSource(doc);
    StringWriter writer = new StringWriter();
    StreamResult result = new StreamResult(writer);
    TransformerFactory tf = TransformerFactory.newInstance();
    Transformer transformer = tf.newTransformer();
    transformer.transform(domSource, result);
    return writer.toString();
  }
}

